Skip to content

Freeze released isolated sessions and project markers (#271) - #291

Merged
memtomem merged 3 commits into
mainfrom
test/frozen-sessions-271
Nov 10, 2026
Merged

memtomem merged 3 commits into
mainfrom
test/frozen-sessions-271

Conversation

@memtomem

Copy link
Copy Markdown
Owner

Refs #271. This is the Sessions and project family. It follows the convention of the artifacts, rooms and workflows families. The issue stays open.

What

  • Generator. tests/fixtures/persisted/sessions/freeze.py writes the fixtures for v0.1.2, v0.2.0 and v0.3.0, each with the release's own CLI.
    • It runs the release's syncmill init. The marker stays untracked and ignored, and syncmill.toml is committed.
    • It then runs session open codex --no-attach against a fake interactive codex in a real tmux.
    • A tmux on the fake PATH execs the real binary with a private socket and no config, so the user's server is never touched.
    • The private server is stopped and proved gone before the box is removed. If that cannot be proved, nothing is removed and the run exits nonzero.
  • Scenarios (per release):
    • project: syncmill.toml plus the marker directory's project.json and .gitignore, stored as marker.json and marker.gitignore. From v0.2.0, init's .gitignore names project.json itself, so Git would ignore the marker under its own name. Its 0600 mode is asserted and recorded.
    • open; checkpointed, with one checkpoint run artifact; closed.
    • creating, closing, recovery-required, from a .pth hook loaded for the run only: an exit on entry to the tmux launch, an exit right after the closing document, and a cleanup that reports failure.
    • Workspaces are not frozen: they are Git repositories holding absolute paths.
  • Rebinding. Each session document's four paths and its fingerprint (kept in its released v1 version) are rebound, and the checkpoint manifest's root and fingerprint, with exact counts. Every other field is asserted unchanged; the tmux name re-derives. tmux is never launched in a test.
  • Readers:
    • session isolated-list (liveness from the probe, staleness in the stored version, open sessions only) and render_session.
    • The path binding: a document under another workspace root, with another tmux name, a worktree outside its run root, or another repository is skipped by list and refused by checkpoint.
    • attach-isolated: refused when the session is not running or a pane is elsewhere, attached to the one right pane, and refused from every non-open state.
    • checkpoint without its worktree.
    • close of every state with the workspace gone: each ends closed at a pinned revision, and a closed one is returned unchanged.
    • The checkpoint artifact:
      • read through runs list / show, diff, and conversations show / list / close;
      • continue refused (no provider session), and recover skipped;
      • apply (left applied with its receipt bound) and validate_applicable;
      • discard, and prune, which keeps it while pending and removes it once discarded;
      • the TUI's render_artifact and _read_artifact_result.
    • The project:
      • project_id with the released marker untracked (its id), and tracked or symlinked (the path hash);
      • load_project_config of the released config, value for value;
      • initialize_project falling back to the marker.
  • docs/architecture/persisted-contracts.md: the row names these. Not covered: a successful checkpoint or worktree cleanup, because a release-written workspace is not relocatable.

No source under src/ changes. No current reader misread a released session or marker.

Verification

  • suite-run CLEAN; ruff check . clean.
  • 16 injected regressions in isolated_session.py, project.py, artifacts.py and worktree.py each fail an intended assertion. They include:
    • each path or name binding removed;
    • liveness or staleness ignored;
    • the pane, state and missing-worktree checks removed;
    • close not idempotent;
    • each marker gate removed, and the re-init fallback dropped;
    • pending prune allowed;
    • the fingerprint recomputed in the current version;
    • the applied flip skipped.
  • The generator's shutdown-failure path was exercised with the shutdown reported as failed: it kept the box and socket and exited nonzero. That leftover was then removed by hand, after confirming the server was down.
  • Codex reviewed the design in two rounds before approval. The implementation went three rounds:
    • Round 1: NEEDS-FIX, fixed in 56535a0.
      • The fingerprint version and the applied state were unchecked.
      • Generator cleanup ran in the wrong order.
      • Release discovery picked up __pycache__.
    • Round 2: NEEDS-FIX, fixed in 1eacaf9.
      • Round 1's cleanup fix still deleted the box on failure.
      • Two config values were unchecked.
      • One timestamp assertion depended on the clock.
    • Round 3: unprimed, given the diff only. SHIP with no findings.

🤖 Generated with Claude Code

memtomem and others added 3 commits November 10, 2026 10:36
The Sessions and project family of the persisted-contracts D rows.

tests/fixtures/persisted/sessions/<release>/ holds what v0.1.2, v0.2.0
and v0.3.0 wrote with their own CLIs. freeze.py runs the release's
`syncmill init` (the marker stays untracked and ignored, syncmill.toml is
committed), then `session open codex --no-attach` against a fake
interactive codex in a real tmux. A tmux on the fake PATH execs the real
one with a private socket and no config, so the user's server is never
touched.
- project: syncmill.toml and the marker directory's project.json and
  .gitignore, stored as marker.json and marker.gitignore: from v0.2.0
  init's .gitignore names project.json, so Git would ignore the marker
  under its own name. Its 0600 mode is asserted and recorded.
- open, checkpointed (one checkpoint run artifact), closed;
- creating, closing, recovery-required: an exit on entry to the tmux
  launch, an exit right after the closing document, and a cleanup that
  reports failure, from a .pth hook loaded for the run only.
Workspaces are not frozen: they are Git repositories holding absolute
paths.

The test rebinds each session document's four paths and fingerprint and
the checkpoint manifest's root and fingerprint, with exact counts, and
asserts every other field unchanged; the tmux name re-derives. tmux is
never launched: the store's probe, commands and attachment and the CLI's
gates are patched. It reads:
- session isolated-list (live from the probe, staleness in the stored
  fingerprint's version, open sessions only), render_session;
- load's path binding: a document under another workspace root, with
  another tmux name, a worktree outside its run root or another
  repository is skipped by list and refused by checkpoint;
- attach-isolated (not running, a pane elsewhere, the one right pane),
  refused from every non-open state; checkpoint without its worktree;
  close of every state with the workspace gone (each ends closed at a
  pinned revision; a closed one is returned unchanged);
- the checkpoint artifact through runs list/show, diff, conversations
  show/list/close, continue (refused: no provider session), recover
  (skipped), apply, validate_applicable, discard and prune (pending
  kept, removed once discarded), and the TUI's render_artifact and
  _read_artifact_result;
- project_id with the released marker untracked (its id), tracked or
  symlinked (the path hash), load_project_config of the released config,
  and initialize_project falling back to the marker.

14 injected regressions in isolated_session.py, project.py and
artifacts.py each fail an intended assertion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 1:
- the installed fingerprint is now asserted to keep the released v1
  version (a recompute that ignored like= upgraded it to v2 before any
  reader saw it, and every test still passed);
- applying the released checkpoint must leave the artifact applied with
  its receipt bound to the apply (skipping the applied flip passed);
- release discovery takes only v*/ directories holding bindings.json
  (importing freeze.py made __pycache__ a "release");
- freeze.py stops its private tmux server before the box is removed and
  proves it gone; if it cannot, the socket is kept and its kill command
  printed. Generation output is unchanged.

16 injected regressions now each fail an intended assertion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 2:
- freeze.py now removes neither the box nor the socket, and exits
  nonzero, when it cannot prove its private tmux server stopped (round
  1's fix still deleted the box and returned success); checked with the
  shutdown reported as failed;
- the released config is also checked for its config_version and
  project.memory_dir;
- conversations close is checked against a pinned clock instead of a
  timestamp that merely differs.

16 injected regressions still each fail an intended assertion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@memtomem
memtomem merged commit d8932db into main Nov 10, 2026
11 checks passed
@memtomem
memtomem deleted the test/frozen-sessions-271 branch November 10, 2026 02:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant